This is a project I have worked on for fun, more or less in my spare time. It is not intended for, and probably shouldn’t be used for, any kind of commercial purposes. The data here comes from the very detailed https://www.bicyclerollingresistance.com/. Much credit to Jarno Bierman for all of the data
Cyclists for years have wondered which is the best tire on the market for their needs. I myself have wondered this as well. Bicycle tires are primarily defined by their speed, measured by rolling resistance (lower is faster) and puncture protection (higher is better). Objective results of testing for tires is often hard to come by, until Jarno Bierman created his bicyclerollingresistance.com. Jarno uses a rolling drum to test tires on their resistance and also a test to determine puncture resistance. I make no comments on the quality of the test and whether or not it is really the best in the world, however other tests, for example from the German magazine RoadBike show directionally similar results, so I will consider these good.
First, I import the data from bicyclerollingresistance.com. Lacking better skills, I copy into a csv file and import into R
Secondly, I process the data to make it into a format more suitable for analysis.
Thirdly, I present some visualizations.
Fourth, we enter our weighting criteria for tire resistance, puncture protection and weight. We do this on a scale from 1-100, with a total of only 100 for all 3 criteria.
Fifth, I normalize the test scores and multiply by the criteria.
Sixth, I score and rank the tires by the total score
Finally, I present a visualization of the results.
## -- Attaching packages ----------------------------------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.2.1 v purrr 0.3.3
## v tibble 2.1.3 v dplyr 0.8.3
## v tidyr 1.0.0 v stringr 1.4.0
## v readr 1.3.1 v forcats 0.4.0
## -- Conflicts -------------------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::between() masks data.table::between()
## x dplyr::filter() masks stats::filter()
## x dplyr::first() masks data.table::first()
## x dplyr::lag() masks stats::lag()
## x dplyr::last() masks data.table::last()
## x purrr::transpose() masks data.table::transpose()
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
#Step 1, import the data
## Brand Model Year TireType PriceRange Width
## 1 Pirelli Cinturato Velo TLR 2018 TL High 26 / 28
## 2 Continental Competition (tubular) 2016 TU High+ 25 / 25
## 3 Veloflex Corsa 2016 TT High 25 / 25
## 4 Vittoria Corsa Control G+ 1.0 (open) 2018 TT High 25 / 27
## 5 Vittoria Corsa Control G+ 2.0 (open) 2019 TT High 25 / 27
## 6 Vittoria Corsa Elite (tubular) 2017 TU High 25 / 25
## Weight RR120 RR100 RR80 RR60 PunctureTest Thickness
## 1 290 / 306 15.6 16.6 18.7 22.3 20 / 7 3.7 / 0.95
## 2 280 / 285 14.2 15.3 16.5 18.7 11 / 4 2.5 / 0.55
## 3 205 / 200 13.4 14.0 15.8 18.2 12 / 4 2.1 / 0.75
## 4 265 / 260 15.6 16.7 18.1 21.1 12 / 5 2.6 / 0.85
## 5 265 / 262 14.1 15.4 17.0 20.1 12 / 5 2.7 / 0.90
## 6 290 / 298 12.9 13.6 15.4 18.1 12 / 3 2.4 / 0.60
## Brand Model Year TireType
## Length:75 Length:75 Min. :2014 Length:75
## Class :character Class :character 1st Qu.:2016 Class :character
## Mode :character Mode :character Median :2017 Mode :character
## Mean :2017
## 3rd Qu.:2018
## Max. :2020
## PriceRange Width Weight RR120
## Length:75 Length:75 Length:75 Min. : 7.00
## Class :character Class :character Class :character 1st Qu.:11.70
## Mode :character Mode :character Mode :character Median :13.40
## Mean :13.49
## 3rd Qu.:15.40
## Max. :19.50
## RR100 RR80 RR60 PunctureTest
## Min. : 7.50 Min. : 8.30 Min. : 9.30 Length:75
## 1st Qu.:12.50 1st Qu.:13.85 1st Qu.:16.35 Class :character
## Median :14.20 Median :16.00 Median :18.70 Mode :character
## Mean :14.35 Mean :16.00 Mean :18.80
## 3rd Qu.:16.60 3rd Qu.:18.55 3rd Qu.:21.80
## Max. :20.50 Max. :23.10 Max. :27.40
## Thickness
## Length:75
## Class :character
## Mode :character
##
##
##
Some of the data is not in a correct type for analysis. Price Range, Tire Type and Brand are all character and before we go further, I will convert these to factor, as each is a discrete group.
Some of the fields we can see are entered as “number / number”. This makes any kind of analysis impossible so I will separate these fields with ‘separate()’ from dplyr.
tires.separated <- tires %>%
separate(PunctureTest, into = c('Punct.T','Punct.Side')) %>%
separate(Thickness, into = c('Thick.Tread','Thick.Side'), convert = F) %>%
separate(Width, into = c('Width.S', 'Width.M')) %>%
separate(Weight, into = c('Weight.S','Weight.M'))
## Warning: Expected 2 pieces. Additional pieces discarded in 72 rows [1, 2, 3, 4,
## 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...].
These new fields need to be as number and now I convert those
tires.separated$Weight.M <- as.numeric(tires.separated$Weight.M, na.rm = TRUE)
tires.separated$Punct.T <- as.numeric(tires.separated$Punct.T)
tires.separated$Thick.Tread <- as.numeric(tires.separated$Thick.Tread, na.rm = TRUE)
tires.separated$Thick.Side <- as.numeric(tires.separated$Thick.Side, na.rm = TRUE)
A quick check of the data:
summary(tires.separated)
## Brand Model Year TireType PriceRange
## Vittoria :16 Length:75 Min. :2014 TL:15 High :50
## Continental:15 Class :character 1st Qu.:2016 TT:55 Medium: 0
## Schwalbe :14 Mode :character Median :2017 TU: 5 Low : 5
## Michelin : 7 Mean :2017 NA's :20
## Pirelli : 4 3rd Qu.:2018
## Hutchinson : 3 Max. :2020
## (Other) :16
## Width.S Width.M Weight.S Weight.M
## Length:75 Length:75 Length:75 Min. :163.0
## Class :character Class :character Class :character 1st Qu.:220.5
## Mode :character Mode :character Mode :character Median :245.0
## Mean :250.3
## 3rd Qu.:267.0
## Max. :399.0
##
## RR120 RR100 RR80 RR60
## Min. : 7.00 Min. : 7.50 Min. : 8.30 Min. : 9.30
## 1st Qu.:11.70 1st Qu.:12.50 1st Qu.:13.85 1st Qu.:16.35
## Median :13.40 Median :14.20 Median :16.00 Median :18.70
## Mean :13.49 Mean :14.35 Mean :16.00 Mean :18.80
## 3rd Qu.:15.40 3rd Qu.:16.60 3rd Qu.:18.55 3rd Qu.:21.80
## Max. :19.50 Max. :20.50 Max. :23.10 Max. :27.40
##
## Punct.T Punct.Side Thick.Tread Thick.Side
## Min. : 6.00 Length:75 Min. :1.000 Min. :0.000
## 1st Qu.:10.00 Class :character 1st Qu.:2.000 1st Qu.:2.000
## Median :11.00 Mode :character Median :2.000 Median :6.000
## Mean :11.43 Mean :2.236 Mean :4.847
## 3rd Qu.:12.00 3rd Qu.:3.000 3rd Qu.:7.000
## Max. :20.00 Max. :4.000 Max. :9.000
## NA's :3 NA's :3
tires.separated %>% datatable(rownames = F)
One piece I have not been able to correct is that tread thickness (Thick.Tread) should be as a double and separate() saves as an integer. This loses some of the fidelity in the data. I will look at correcting as I learn more R… :)
We can see a very clear trend between rolling resistance and puncture resistance: the lower the rolling resistance (i.e. the faster the tire), the lower the puncture resistance. The data bears out the logical notion that there is a trade-off between speed and puncture protection
Note that the echo = FALSE parameter was added to the code chunk to prevent printing of the R code that generated the plot.